5.04. Справочник про F#
Справочник про F#
Основы синтаксиса, типы и значения
1. Общие принципы языка
F# — это строго типизированный, функциональный язык программирования, совместимый с платформой .NET. Он поддерживает иммутабельность по умолчанию, вывод типов, сопоставление с образцом, композицию функций и выражения вместо операторов.
Код в F# организован в выражения, а не в последовательности команд. Каждое выражение имеет значение и тип.
2. Базовые типы
Примитивные типы
bool— логический тип (true,false)int— 32-битное целое числоint8,int16,int32,int64— целые числа с явным указанием размераuint8,uint16,uint32,uint64— беззнаковые целыеfloat— 64-битное число с плавающей точкой (IEEE 754 double)float32— 32-битное число с плавающей точкой (IEEE 754 single)decimal— десятичное число с высокой точностью для финансовых вычисленийchar— одиночный символ Unicodestring— неизменяемая строка Unicodeunit— тип с единственным значением(), используется как «ничего не возвращается»
Составные типы
-
Кортежи (tuples) — фиксированные наборы значений разных типов
Пример:(1, "hello", true)
Тип:int * string * bool -
Списки (lists) — односвязные иммутабельные списки одного типа
Пример:[1; 2; 3]
Тип:int list
Пустой список:[]
Конструктор:head :: tail
Операторы:@— конкатенация списков -
Массивы (arrays) — изменяемые последовательности фиксированной длины
Пример:[|1; 2; 3|]
Тип:int[]илиint array -
Последовательности (sequences) — ленивые потоки значений
Пример:seq { 1 .. 10 }
Тип:seq<int> -
Записи (records) — именованные структуры с полями
type Person = { Name: string; Age: int }
let p = { Name = "Alice"; Age = 30 } -
Размеченные объединения (discriminated unions) — типы с несколькими вариантами
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let s = Circle 5.0 -
Опции (option) — стандартный тип для представления наличия или отсутствия значения
let x: int option = Some 42
let y: int option = None -
Результат (Result) — тип для обработки успешных и ошибочных результатов
type Result<'T, 'Error> =
| Ok of 'T
| Error of 'Error
3. Литералы и синтаксические формы
Числовые литералы
- Целые:
42,-17,0x1A(шестнадцатеричные),0b1010(двоичные) - Вещественные:
3.14,1e-5,2.5f(суффиксfдляfloat32) - Десятичные:
123.45m(суффиксm)
Строковые литералы
- Обычные:
"Hello" - Многострочные:
"""Line 1\nLine 2""" - Вербальные (verbatim):
@"C:\path\to\file"— обратные слэши не экранируются - Интерполированные:
$"Hello, {name}!"
Символьные литералы
'a','\n','\t','\u0041'
4. Имена и привязки
В F# используются привязки (let) вместо присваивания.
let x = 42
let y = x + 1
Привязки иммутабельны по умолчанию. Для создания изменяемых значений используется ключевое слово mutable:
let mutable counter = 0
counter <- counter + 1
Оператор <- используется для изменения значения mutable переменной.
5. Функции
Функции определяются через let:
let add a b = a + b
Тип выводится автоматически: val add : int -> int -> int
Функции — значения первого класса. Их можно передавать, возвращать, хранить в структурах.
Каррирование
Функция add каррирована: add 1 возвращает новую функцию int -> int.
Явное указание типов
let add (a: int) (b: int) : int = a + b
Анонимные функции
let square = fun x -> x * x
List.map (fun x -> x * 2) [1; 2; 3]
Рекурсия
Для рекурсивных функций требуется rec:
let rec factorial n =
if n <= 1 then 1 else n * factorial (n - 1)
Хвостовая рекурсия
F# оптимизирует хвостовую рекурсию в цикл. Используется аккумулятор:
let factorial n =
let rec loop acc i =
if i <= 1 then acc else loop (acc * i) (i - 1)
loop 1 n
6. Сопоставление с образцом (Pattern Matching)
Основной механизм ветвления и деструктуризации:
let describeNumber x =
match x with
| 0 -> "zero"
| 1 -> "one"
| n when n > 0 -> "positive"
| _ -> "negative or other"
Поддерживает:
- литералы
- переменные
- условия (
when) - кортежи
- списки (
head :: tail) - записи
- размеченные объединения
Пример с кортежем:
let swap (a, b) = (b, a)
Пример с записью:
let greet { Name = name } = $"Hello, {name}!"
7. Операторы
F# использует следующие операторы:
- Арифметические:
+,-,*,/,% - Сравнения:
=,<>,<,<=,>,>= - Логические:
&&,||,not - Битовые:
&&&,|||,^^^,~~~,<<<,>>> - Списковые:
::(конструктор),@(конкатенация) - Присваивания:
<-(только дляmutable) - Типизации:
:(аннотация типа),:>(преобразование к базовому типу),:?>(динамическое приведение)
Операторы можно определять как функции:
let (+.) a b = sqrt(a ** 2.0 + b ** 2.0)
8. Комментарии
- Однострочные:
// комментарий - Многострочные:
(* комментарий *)
9. Модульность
Код организуется в модули и пространства имён.
namespace MyNamespace
module MathUtils =
let pi = 3.14159
let circleArea r = pi * r * r
Импорт других модулей:
open System
open List
10. Типобезопасность и вывод типов
F# использует алгоритм Хиндли-Милнера для вывода типов. Большинство типов не нужно указывать явно.
Компилятор гарантирует:
- Отсутствие null-ссылок (кроме взаимодействия с .NET)
- Безопасность шаблонов (все случаи размеченного объединения должны быть обработаны)
- Иммутабельность по умолчанию
Коллекции, последовательности, LINQ-подобные операции, асинхронность и взаимодействие с .NET
1. Коллекции и стандартные модули
F# предоставляет богатый набор функций для работы с коллекциями через модули List, Array, Seq, Map, Set.
Модуль List
Основные функции:
List.map f xs— преобразует каждый элемент спискаList.filter p xs— оставляет элементы, удовлетворяющие предикатуList.fold f acc xs— сворачивает список слева направоList.reduce f xs— сворачивает непустой список без начального аккумулятораList.iter f xs— применяет действие к каждому элементу (для побочных эффектов)List.length xs— возвращает длину спискаList.isEmpty xs— проверяет, пуст ли списокList.rev xs— разворачивает списокList.append xs ys— объединяет два списка (xs @ ys)List.concat xss— объединяет список списков в один списокList.exists p xs— проверяет наличие элемента, удовлетворяющего условиюList.forall p xs— проверяет, что все элементы удовлетворяют условиюList.tryFind p xs— возвращаетSome x, если найден элемент, иначеNoneList.partition p xs— разделяет список на два: удовлетворяющие и неудовлетворяющие предикатуList.zip xs ys— объединяет два списка в список парList.unzip pairs— разделяет список пар на два списка
Модуль Array
Аналогичен List, но работает с изменяемыми массивами:
Array.map,Array.filter,Array.iter,Array.lengthи т.д.Array.create n v— создаёт массив длиныn, заполненный значениемvArray.zeroCreate n— создаёт массив с нулевыми значениями (для числовых типов) илиnull(для ссылочных типов)Array.set arr i v— устанавливает значение по индексуArray.get arr i— получает значение по индексу
Модуль Seq (последовательности)
Ленивые вычисления. Подходят для бесконечных потоков или больших данных.
Seq.init n f— создаёт последовательность изnэлементов, генерируемых функциейSeq.unfold f state— генерирует последовательность из состояния (аналогwhile)Seq.map,Seq.filter,Seq.iter— аналогично спискам, но ленивоSeq.cache s— кэширует результаты для повторного использованияSeq.toArray,Seq.toList— материализуют последовательность
Пример бесконечной последовательности:
let naturals = Seq.initInfinite id // 0, 1, 2, 3, ...
let evens = Seq.filter (fun x -> x % 2 = 0) naturals
let first10Evens = evens |> Seq.take 10 |> Seq.toList
2. Композиция и конвейеры
F# активно использует оператор конвейера |>:
[1..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * x)
|> List.sum
Эквивалентно:
List.sum (List.map (fun x -> x * x) (List.filter (fun x -> x % 2 = 0) [1..10]))
Также существует обратный конвейер <|:
printfn "%d" <| List.sum [1; 2; 3]
Полезен, чтобы избежать скобок при вызове функции с одним аргументом.
3. Словари и множества
Map<'Key, 'Value>
Иммутабельный ассоциативный массив на основе сбалансированного дерева.
let phoneBook = Map [("Alice", "123"); ("Bob", "456")]
let aliceNumber = Map.find "Alice" phoneBook
let updated = Map.add "Charlie" "789" phoneBook
Функции: Map.add, Map.remove, Map.containsKey, Map.find, Map.tryFind, Map.change, Map.map, Map.fold.
Set<'T>
Иммутабельное множество.
let primes = Set [2; 3; 5; 7]
let hasThree = Set.contains 3 primes
let morePrimes = Set.add 11 primes
Функции: Set.add, Set.remove, Set.contains, Set.union, Set.intersect, Set.difference.
Оба типа требуют, чтобы ключи/элементы реализовывали сравнение (comparison constraint).
4. Работа с .NET
F# полностью совместим с .NET. Можно использовать любые классы, интерфейсы, события.
Создание объектов
let list = new System.Collections.Generic.List<int>()
list.Add(42)
Или без new:
let dict = System.Collections.Generic.Dictionary<string, int>()
dict.["age"] <- 30
Свойства и индексаторы
let sb = System.Text.StringBuilder()
sb.Append("Hello") |> ignore
let text = sb.ToString()
Индексаторы используются как obj.[key].
Исключения
try
riskyOperation()
with
| :? System.IO.FileNotFoundException as ex ->
printfn "File not found: %s" ex.Message
| ex ->
printfn "Error: %s" ex.Message
Выброс исключения:
failwith "Something went wrong"
invalidArg "paramName" "Invalid argument"
5. Асинхронность
F# имеет встроенную поддержку асинхронных вычислений через асинхронные рабочие процессы (async).
Базовый синтаксис
let fetchUrlAsync url =
async {
let webClient = new System.Net.WebClient()
let! html = webClient.AsyncDownloadString(System.Uri(url))
return html.Length
}
Запуск:
let result = fetchUrlAsync "https://example.com" |> Async.RunSynchronously
Или параллельно:
let urls = ["https://a.com"; "https://b.com"]
let tasks = List.map fetchUrlAsync urls
let results = Async.Parallel tasks |> Async.RunSynchronously
Ключевые конструкции:
let!— ожидает завершения асинхронной операцииdo!— выполняет асинхронное действие без привязки результатаreturn— завершает асинхронный блок с результатомuse/use!— автоматически освобождает ресурс после выхода из блока
Преобразование из Task (.NET)
Для работы с Task<T> из C# библиотек:
open System.Threading.Tasks
let taskToAsync (task: Task<'T>) : Async<'T> = Async.AwaitTask task
Или напрямую:
let! result = someCSharpMethodReturningTask() |> Async.AwaitTask
6. Обработка ошибок без исключений
F# поощряет использование Option и Result вместо исключений.
Пример с Result:
let divide a b =
if b = 0 then Error "Division by zero" else Ok (a / b)
match divide 10 2 with
| Ok value -> printfn "Result: %d" value
| Error msg -> printfn "Error: %s" msg
Цепочка операций:
let safeDivideChain =
divide 100 10
|> Result.bind (divide 10)
|> Result.bind (divide 2)
Модуль Result содержит: map, bind, flatMap, either, isOk, isError.
7. Интерполяция и форматирование строк
F# поддерживает интерполированные строки:
let name = "Alice"
let age = 30
printfn $"Hello, {name}! You are {age} years old."
Также можно указывать форматы:
let pi = 3.14159
printfn $"Pi ≈ {pi:F2}" // Pi ≈ 3.14
Альтернатива — sprintf для создания строки:
let message = sprintf "Value: %d" 42
8. Единицы измерения (Units of Measure)
F# позволяет прикреплять физические единицы к числам для статической проверки:
[<Measure>] type m
[<Measure>] type s
let distance = 10<m>
let time = 2<s>
let speed = distance / time // тип: float<m/s>
Эта функция работает только с float, float32, decimal.
9. Типовые ограничения и обобщения
F# поддерживает параметрический полиморфизм:
let identity x = x // тип: 'a -> 'a
let swap (a, b) = (b, a) // тип: 'a * 'b -> 'b * 'a
Ограничения:
'T : equality— тип должен поддерживать='T : comparison— тип должен поддерживать<'T : null— тип может бытьnull'T : not struct— ссылочный тип'T : struct— тип значения
Пример:
let inline add a b = a + b // работает благодаря SRTP (statically resolved type parameters)
10. Взаимодействие с другими языками .NET
F# компилируется в тот же IL-код, что и C#. Библиотеки на F# можно использовать из C#, и наоборот.
Чтобы сделать F# тип удобным для C#:
- Использовать
[<CLIMutable>]для записей (разрешает мутацию через свойства) - Использовать
[<RequireQualifiedAccess>]для модулей, чтобы избежать конфликтов имён - Использовать
[<AutoOpen>]для автоматического открытия модуля
Пример:
[<CLIMutable>]
type Person = { Name: string; Age: int }
Теперь в C# можно писать:
var p = new Person { Name = "Alice", Age = 30 };
Модули, пространства имён, компиляция, настройки проекта, инструменты и продвинутые возможности языка
1. Организация кода: пространства имён и модули
F# использует два основных механизма для группировки кода:
- Пространства имён (
namespace) — лёгкие контейнеры без значений, только типы и модули. - Модули (
module) — могут содержать значения, функции, типы и вложенные модули.
Пространство имён
namespace MyLibrary
type Point = { X: float; Y: float }
module Geometry =
let distance p1 p2 =
sqrt ((p1.X - p2.X) ** 2.0 + (p1.Y - p2.Y) ** 2.0)
Файл с namespace не может содержать код верхнего уровня вне типа или модуля.
Модуль верхнего уровня
module MathUtils
let pi = 3.14159
let square x = x * x
Эквивалентно:
namespace MyProject
module MathUtils = ...
Вложенные модули
module Outer =
module Inner =
let value = 42
let getValue () = Inner.value
Использование извне:
open Outer
let x = Inner.value
Квалифицированный доступ
Если модуль помечен атрибутом [<RequireQualifiedAccess>], его функции можно вызывать только с указанием имени модуля:
[<RequireQualifiedAccess>]
module List =
let customMap f xs = ... // не конфликтует со стандартным List.map
Вызов: List.customMap f xs, а не просто customMap f xs.
Автоматическое открытие
Атрибут [<AutoOpen>] добавляет содержимое модуля в область видимости при открытии родительского пространства имён:
[<AutoOpen>]
module Helpers =
let inline (!!) x = not x
После open MyNamespace оператор !! становится доступен.
2. Файловая структура и порядок компиляции
F# чувствителен к порядку файлов в проекте. Каждый файл может ссылаться только на предыдущие файлы.
Файлы перечисляются в .fsproj в порядке компиляции:
<ItemGroup>
<Compile Include="Types.fs" />
<Compile Include="Core.fs" />
<Compile Include="Api.fs" />
</ItemGroup>
В Types.fs определяются базовые типы, в Core.fs — логика, в Api.fs — интерфейсы.
Нарушение порядка вызывает ошибку компиляции: «The value/namespace 'X' is not defined».
3. Настройки проекта (.fsproj)
Проект F# описывается в файле .fsproj (MSBuild). Основные параметры:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType> <!-- или Library -->
<WarnOn>3390;$(WarnOn)</WarnOn> <!-- дополнительные предупреждения -->
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>FS0020</NoWarn> <!-- подавление конкретного предупреждения -->
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Core" Version="8.0.401" />
</ItemGroup>
</Project>
Важные свойства
TargetFramework— целевая версия .NET (net6.0,net8.0,netstandard2.1)OutputType—Exe(консольное приложение),Library(библиотека),WinExe(GUI без консоли)OtherFlags— передача флагов компилятору F#:<OtherFlags>--warnon:1182 --optimize+</OtherFlags>
Компиляторные флаги F#
Через OtherFlags можно задать:
--optimize+— включить оптимизации--debug+— включить отладочную информацию--tailcalls+— включить оптимизацию хвостовых вызовов (по умолчанию включена в Release)--checked+— включить проверку переполнения (по умолчанию выключена)--warnon:1182— включить предупреждение об неиспользуемых переменных--nowarn:FS0020— отключить предупреждение FS0020
4. Инструменты разработки
.NET CLI
Создание проекта:
dotnet new console -lang F# -n MyProject
dotnet build
dotnet run
Visual Studio / Visual Studio Code
- Visual Studio — полная поддержка F# с IntelliSense, отладчиком, рефакторингом.
- VS Code + Ionide — лёгкая среда с поддержкой F# через расширение Ionide-fsharp.
Ionide предоставляет:
- Подсветку синтаксиса
- Переход к определению
- Просмотр типов
- Запуск REPL (F# Interactive)
F# Interactive (FSI)
Интерактивный REPL запускается командой:
dotnet fsi
Внутри можно выполнять выражения:
> let x = 42;;
val x : int = 42
> [1..5] |> List.map ((*) 2);;
val it : int list = [2; 4; 6; 8; 10]
Загрузка файла:
#load "MyModule.fs";;
#load "Script.fsx";;
Скрипты используют расширение .fsx и могут содержать директивы #r (ссылки) и #load.
Пример скрипта:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let data = {| Name = "Alice"; Age = 30 |}
printfn "%s" (JsonConvert.SerializeObject(data))
5. Продвинутые возможности языка
Активные шаблоны (Active Patterns)
Позволяют расширять сопоставление с образцом пользовательской логикой.
Полные активные шаблоны
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd
let describe n =
match n with
| Even -> "even"
| Odd -> "odd"
Частичные активные шаблоны
let (|Int|_|) str =
match System.Int32.TryParse(str) with
| (true, n) -> Some n
| _ -> None
match "123" with
| Int n -> printfn "Parsed: %d" n
| _ -> printfn "Not a number"
Параметризованные шаблоны
let (|DivisibleBy|_|) divisor n =
if n % divisor = 0 then Some () else None
match 15 with
| DivisibleBy 5 () -> "Divisible by 5"
| _ -> "Not divisible"
Вычислительные выражения (Computation Expressions)
Обобщённый механизм для создания DSL. Используется в async, seq, task, option, result.
Пример: кастомный OptionBuilder:
type OptionBuilder() =
member _.Bind(x, f) = Option.bind f x
member _.Return(x) = Some x
member _.ReturnFrom(x) = x
member _.Zero() = Some ()
let option = OptionBuilder()
let safeDivide a b =
if b = 0 then None else Some (a / b)
let compute a b c =
option {
let! x = safeDivide a b
let! y = safeDivide x c
return y
}
Вызов: compute 100 10 2 → Some 5; compute 100 0 2 → None.
Типы-провайдеры (Type Providers)
Генерируют типы во время компиляции на основе внешних данных (CSV, JSON, API, базы данных).
Пример с CSV:
#r "nuget: FSharp.Data"
open FSharp.Data
type Sales = CsvProvider<"sample.csv">
let sales = Sales.Load("data.csv")
for row in sales.Rows do
printfn "%s: %f" row.Product row.Price
Тип Sales создаётся автоматически, с полями, соответствующими столбцам CSV.
Другие провайдеры:
JsonProviderXmlProviderSqlDataProvider(для SQL Server)ODataService
Типы-провайдеры требуют поддержки IDE и работают только в режиме разработки (design-time).
Измерения (Statically Resolved Type Parameters)
Позволяют писать обобщённый код с ограничениями на наличие методов или операторов:
let inline addAndLog x y =
let result = x + y
printfn "Computed: %A" result
result
Тип: 'a -> 'a -> 'a when 'a : (static member (+) : 'a -> 'a -> 'a)
Работает с любыми типами, поддерживающими +.
6. Производительность и оптимизации
- Хвостовая рекурсия — компилируется в цикл, не потребляет стек.
- Иммутабельные структуры — безопасны, но могут создавать накладные расходы при частом копировании.
- Массивы вместо списков — для интенсивной обработки данных.
inline— подставляет тело функции, устраняя вызов (полезно для SRTP).- Избегание замыканий в горячих циклах — они выделяют память.
7. Тестирование
F# совместим с xUnit, NUnit, FsCheck.
Пример с xUnit:
open Xunit
[<Fact>]
let ``addition works`` () =
Assert.Equal(4, 2 + 2)
FsCheck — генерация случайных данных:
open FsCheck
let revRevIsId xs = List.rev (List.rev xs) = xs
Check.QuickThrowOnFailure revRevIsId
Практические паттерны проектирования, архитектура приложений, работа с файлами, сетью, сериализацией и реальные сценарии использования
1. Архитектурные подходы в F#
F# поддерживает функциональную архитектуру, основанную на чистых функциях, иммутабельных данных и композиции. Часто применяются следующие паттерны:
Onion Architecture / Hexagonal Architecture
- Ядро приложения — доменные типы и чистые функции.
- Внешние слои — адаптеры для ввода-вывода (файлы, сеть, базы данных).
- Зависимости направлены внутрь: инфраструктура зависит от ядра, а не наоборот.
Пример структуры:
Domain/ — доменные типы и логика
Application/ — use cases, сервисы
Infrastructure/ — файлы, HTTP, базы данных
Interfaces/ — интерфейсы для DI (если используется)
Program.fs — точка входа, компоновка
Pipeline-обработка
Данные проходят через цепочку преобразований:
let processOrder =
validateOrder
>> enrichOrder
>> calculateTotal
>> applyDiscount
>> persistOrder
Каждая функция принимает Result<Order, Error> и возвращает Result<Order, Error>, что позволяет легко обрабатывать ошибки.
Модель «Smart Constructors»
Конструкторы типа скрывают прямое создание, обеспечивая инварианты:
module Email =
type T = private Email of string
let create (s: string) =
if System.Text.RegularExpressions.Regex.IsMatch(s, @"^.+@.+\..+$")
then Ok (Email s)
else Error "Invalid email"
let value (Email s) = s
Тип Email.T нельзя создать напрямую — только через Email.create.
2. Обработка ошибок в реальных приложениях
F# поощряет явное моделирование ошибок через Result<'T, 'Error>.
Типизированные ошибки
type OrderError =
| InvalidCustomerId
| OutOfStock of productId: string
| PaymentDeclined
type Result<'T> = Result<'T, OrderError>
Комбинаторы для цепочек
let bind f x =
match x with
| Ok v -> f v
| Error e -> Error e
let (>>=) = bind
Использование:
validateCustomerId id
>>= loadCustomer
>>= checkInventory
>>= chargePayment
>>= confirmOrder
Если любой шаг вернёт Error, цепочка останавливается.
Логирование и восстановление
Можно добавить логирование без нарушения потока:
let logAndContinue onError result =
match result with
| Error e ->
logError e
onError e
| Ok _ -> result
3. Работа с файловой системой
F# использует стандартные .NET API, но с функциональным стилем.
Чтение файла
open System.IO
let readAllLines path =
try
File.ReadAllLines(path) |> Array.toList |> Ok
with
| :? IOException as ex -> Error ex.Message
Безопасная запись
let writeText path content =
async {
try
do! File.WriteAllTextAsync(path, content) |> Async.AwaitTask
return Ok ()
with
| :? IOException as ex -> return Error ex.Message
}
Работа с путями
let configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "myapp", "config.json")
4. Сетевые операции
HTTP-запросы с HttpClient
open System.Net.Http
let httpClient = new HttpClient()
let fetchJsonAsync (url: string) : Async<string> =
async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
if response.IsSuccessStatusCode then
let! content = response.Content.ReadAsStringAsync() |> Async.AwaitTask
return content
else
return failwith $"HTTP {response.StatusCode}"
}
Асинхронный сервер с Suave (лёгкий веб-фреймворк)
open Suave
open Suave.Filters
open Suave.Operators
let app =
choose [
GET >=> path "/hello" >=> Successful.OK "Hello from F#!"
POST >=> path "/echo" >=> Request.streamReader >>= fun body -> Successful.OK body
]
[<EntryPoint>]
let main _ =
WebApp.startWebServer defaultConfig app
0
5. Сериализация и десериализация
JSON с System.Text.Json
open System.Text.Json
type Person = { Name: string; Age: int }
let serialize (p: Person) = JsonSerializer.Serialize(p)
let deserialize (json: string) = JsonSerializer.Deserialize<Person>(json)
Для корректной работы с записями рекомендуется [<CLIMutable>]:
[<CLIMutable>]
type Person = { Name: string; Age: int }
JSON с Newtonsoft.Json
Поддерживает неизменяемые записи без атрибутов:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let settings = JsonSerializerSettings(ContractResolver = CamelCasePropertyNamesContractResolver())
let json = JsonConvert.SerializeObject(person, settings)
let person = JsonConvert.DeserializeObject<Person>(json, settings)
CSV с FSharp.Data
type Sales = CsvProvider<"sales.csv">
let data = Sales.Load("actual_sales.csv")
for row in data.Rows do
printfn "%s sold %d units" row.Product row.Units
6. Параллелизм и конкурентность
Параллельное выполнение задач
let urls = ["https://a.com"; "https://b.com"; "https://c.com"]
let fetchAll =
urls
|> List.map fetchUrlAsync
|> Async.Parallel
|> Async.RunSynchronously
Агенты (MailboxProcessor)
Лёгковесный актор для управления состоянием:
type Message =
| Add of int
| Get of AsyncReplyChannel<int>
let counterAgent = MailboxProcessor.Start(fun inbox ->
let rec loop total =
async {
let! msg = inbox.Receive()
match msg with
| Add n -> return! loop (total + n)
| Get reply ->
reply.Reply(total)
return! loop total
}
loop 0)
// Использование
counterAgent.Post(Add 5)
let current = counterAgent.PostAndReply(fun reply -> Get reply)
7. Тестирование и отладка
Юнит-тесты с xUnit
[<Fact>]
let ``Email creation rejects invalid format`` () =
let result = Email.create "not-an-email"
match result with
| Error _ -> ()
| Ok _ -> Assert.Fail("Should not succeed")
Отладка в VS Code
- Установите Ionide.
- Добавьте
.vscode/launch.json:{
"version": "0.2.0",
"configurations": [
{
"name": "Launch F#",
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/bin/Debug/net8.0/MyApp.dll",
"cwd": "${workspaceFolder}"
}
]
} - Ставьте точки останова в
.fsфайлах.
8. Распространённые сценарии использования F#
Анализ данных
- Чтение CSV/JSON → фильтрация → агрегация → экспорт.
- Использование
Deedle(библиотека для анализа данных, аналог Pandas).
Микросервисы
- Чистая бизнес-логика + асинхронный ввод-вывод.
- Лёгкая сборка в Docker-образ.
Скрипты автоматизации
.fsxфайлы для обработки логов, генерации отчётов, миграций.
Финансовые расчёты
- Типы с единицами измерения (
USD,EUR). - Безопасные вычисления с
decimal.
Компиляторы и парсеры
- Размеченные объединения для AST.
- Парсеры с
FParsec.
Инструменты командной строки, CI/CD, упаковка приложений, Docker, производительность, профилирование и лучшие практики
1. Командная строка и .NET CLI
F# полностью интегрирован в экосистему .NET. Основные команды:
Создание проекта
dotnet new console -lang F# -n MyApp
dotnet new classlib -lang F# -n MyLib
dotnet new web -lang F# -n MyWebApp # требует шаблонов (например, Giraffe)
Управление зависимостями
Добавление пакета:
dotnet add package FSharp.Data --version 6.4.0
Установка глобального инструмента (например, FAKE):
dotnet tool install fake-cli --global
Сборка и запуск
dotnet build
dotnet run
dotnet publish -c Release -r linux-x64 --self-contained true
Флаги публикации:
-c Release— оптимизированная сборка-r linux-x64— целевая платформа (win-x64, osx-arm64 и т.д.)--self-contained true— включает .NET runtime в дистрибутив
Результат публикации — автономный исполняемый файл или папка с приложением.
2. Непрерывная интеграция и доставка (CI/CD)
GitHub Actions
Пример .github/workflows/ci.yml:
name: Build and Test
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
Для публикации артефактов:
- name: Publish
run: dotnet publish -c Release -o ./publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: myapp
path: ./publish/
Azure DevOps, GitLab CI
Аналогичные шаги: восстановление, сборка, тестирование, публикация.
3. Упаковка и распространение
Самодостаточные приложения
Команда:
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
Параметры:
PublishSingleFile=true— объединяет всё в один.exeIncludeNativeLibrariesForSelfExtract=true— ускоряет первый запуск
Результат: один исполняемый файл (~70–100 МБ), не требующий установки .NET.
NuGet-пакеты
Создание библиотеки как NuGet-пакета:
В .fsproj:
<PropertyGroup>
<PackageId>MyFSharpLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>YourName</Authors>
<Description>A useful F# library</Description>
<PackageProjectUrl>https://github.com/you/MyLib</PackageProjectUrl>
<RepositoryUrl>https://github.com/you/MyLib</RepositoryUrl>
</PropertyGroup>
Сборка пакета:
dotnet pack -c Release
Публикация:
dotnet nuget push bin/Release/MyFSharpLibrary.1.0.0.nupkg --api-key YOUR_KEY --source https://api.nuget.org/v3/index.json
4. Контейнеризация с Docker
Минимальный Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-self-contained
FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./MyApp"]
Для самодостаточного приложения:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o /app
FROM scratch
COPY --from=build /app/MyApp /MyApp
ENTRYPOINT ["/MyApp"]
Образ на базе scratch занимает всего ~30–50 МБ.
Запуск
docker build -t myapp .
docker run --rm myapp
5. Производительность и профилирование
Советы по производительности
- Используйте массивы вместо списков для больших объёмов данных.
- Избегайте частого создания замыканий в циклах.
- Применяйте
inlineдля маленьких функций с SRTP. - Включайте оптимизации:
<Optimize>true</Optimize>в.fsproj. - Используйте
Span<T>иMemory<T>для работы с буферами без выделения памяти (черезSystem.Memory).
Профилирование
-
Visual Studio Profiler — CPU, память, горячие пути.
-
dotTrace / dotMemory от JetBrains — детальный анализ.
-
perfcollect (Linux):
sudo perfcollect collect MySession
# после завершения
sudo perfcollect view MySession -
BenchmarkDotNet — микро-бенчмарки:
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
type Bench() =
let data = [|1..10000|]
[<Benchmark>]
member _.ListSum() = data |> Array.toList |> List.sum
[<Benchmark>]
member _.ArraySum() = Array.sum data
BenchmarkRunner.Run<Bench>() |> ignore
6. Лучшие практики разработки на F#
Стиль кода
- Используйте иммутабельность по умолчанию.
- Предпочитайте выражения операторам.
- Избегайте
null; используйтеOption. - Не используйте исключения для управления потоком; применяйте
Result. - Делайте функции чистыми (без побочных эффектов) там, где возможно.
Именование
- Типы, модули, пространства имён —
PascalCase. - Функции, значения —
camelCase. - Активные шаблоны —
PascalCaseвнутри(|...|).
Обработка ошибок
- Моделируйте возможные состояния явно через типы.
- Используйте
Errorс конкретными случаями, а не строками. - Цепочки обработки — через
bindили вычислительные выражения.
Тестирование
- Покрывайте чистые функции юнит-тестами.
- Используйте property-based testing (FsCheck) для инвариантов.
- Тестируйте граничные случаи: пустые списки, нулевые значения, переполнения.
Документирование
-
Используйте XML-комментарии:
/// <summary>
/// Calculates the factorial of a non-negative integer.
/// </summary>
/// <param name="n">Input number (must be >= 0)</param>
/// <returns>The factorial of n</returns>
let rec factorial n = ... -
Генерируйте документацию:
<GenerateDocumentationFile>true</GenerateDocumentationFile>
7. Экосистема и популярные библиотеки
| Категория | Библиотека | Назначение |
|---|---|---|
| Веб | Giraffe, Saturn, Suave | Функциональные веб-фреймворки |
| HTTP-клиент | Http.fs, FSharp.Http | Лёгкие клиенты поверх HttpClient |
| JSON | Newtonsoft.Json, System.Text.Json | Сериализация |
| CSV/HTML/XML | FSharp.Data | Типы-провайдеры |
| Парсинг | FParsec | Комбинаторный парсер |
| Тестирование | FsCheck, Expecto | Property-based и assertion-тесты |
| Сборка | FAKE | Автоматизация сборки на F# |
| Машинное обучение | ML.NET, DiffSharp | Интеграция с .NET ML и автоматическое дифференцирование |
| GUI | Fabulous (для Xamarin), Avalonia + Elmish | Кроссплатформенные интерфейсы |
Версии языка, совместимость, миграция, поддержка платформ и ресурсы
1. Версии F# и их ключевые особенности
F# развивается в рамках .NET и публикуется как часть SDK. Каждая версия F# привязана к определённой версии .NET.
| Версия F# | Год | Основные возможности |
|---|---|---|
| F# 1.0 | 2005 | Базовый функционал: записи, размеченные объединения, сопоставление с образцом |
| F# 2.0 | 2010 | Включение в Visual Studio; асинхронные рабочие процессы (async) |
| F# 3.0 | 2012 | Типы-провайдеры, единицы измерения, улучшенный вывод типов |
| F# 4.0 | 2015 | Улучшения в интеропе с C#, nameof, open на уровне класса |
| F# 4.1 | 2017 | [<Struct>] для размеченных объединений и записей, byref, Result в ядре |
| F# 4.5 | 2018 | Span-безопасность, fixed выражения, улучшенная производительность |
| F# 4.7 | 2019 | Неявные операторы преобразования, улучшенный синтаксис для списков |
| F# 5.0 | 2020 | Интерполяция строк ($""), nameof, open static, улучшения в FSI |
| F# 6.0 | 2021 | task вычислительные выражения, try/with в выражениях, улучшенный синтаксис для кортежей |
| F# 7.0 | 2022 | Улучшенная поддержка атрибутов, required поля в записях, лучшая интеграция с C# 11 |
| F# 8.0 | 2023 | Обязательные поля в записях без инициализации, улучшенная работа с null, Discriminated Unions как struct по умолчанию в некоторых контекстах |
Примечание: Начиная с .NET 5, версии F# и .NET синхронизированы: .NET 6 → F# 6.0, .NET 7 → F# 7.0, .NET 8 → F# 8.0.
2. Совместимость между версиями
- Обратная совместимость: код, написанный на F# 4.x, компилируется на F# 8.0 без изменений.
- Прямая совместимость отсутствует: использование новых возможностей (например,
task { }) требует F# 6.0+. - FSharp.Core: основная библиотека времени выполнения. Рекомендуется использовать последнюю стабильную версию, совместимую с целевой платформой.
В .fsproj явно указывается зависимость:
<PackageReference Update="FSharp.Core" Version="8.0.401" />
Это позволяет использовать новые API даже при таргетинге на старые версии .NET (в пределах разумного).
3. Поддержка платформ
F# работает на всех платформах, поддерживаемых .NET:
| Платформа | Поддержка | Особенности |
|---|---|---|
| .NET 8 / .NET 7 / .NET 6 | Полная | Рекомендуемые целевые платформы |
| .NET Standard 2.1 | Полная | Для библиотек, совместимых с .NET Core 3.0+ |
| .NET Framework 4.8 | Частичная | Требуется FSharp.Core 4.7 или ниже; нет поддержки Span<T>, task и других современных функций |
| Mono | Полная | Используется в Xamarin, Unity (ограниченно) |
| WebAssembly (Blazor) | Полная | Через .NET 6+ и Blazor WebAssembly |
| Linux / macOS / Windows | Полная | Один и тот же код компилируется без изменений |
Рекомендация: для новых проектов используйте .NET 8 и F# 8.0.
4. Миграция проектов
С .NET Framework на .NET 8
- Создайте новый проект:
dotnet new console -lang F# -n MyMigratedApp - Перенесите файлы
.fsв правильном порядке. - Обновите зависимости:
- Замените
packages.configнаPackageReference. - Обновите NuGet-пакеты до версий, совместимых с .NET 8.
- Замените
- Удалите устаревшие API:
Async.Start→Async.StartImmediateилиTask.RunList.toArray→List.toArray(остался, но проверьте типы)
- Включите современные флаги:
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
С F# 4.x на F# 8.0
- Замените
async.Returnнаreturnвasync { }. - Используйте
task { }вместоAsync.AwaitTask >> Async.RunSynchronouslyдля взаимодействия с C#. - Замените
failwithв горячих путях наResult.Error, если возможно. - Обновите синтаксис строк:
"Hello " + name→$"Hello {name}".
5. Интеграция с другими языками .NET
Из C# в F#
- F# сборки видны в C# как обычные .NET-библиотеки.
- Записи с
[<CLIMutable>]работают как классы с публичными свойствами. - Размеченные объединения доступны как абстрактные базовые классы с наследниками.
Из F# в C#
- Используйте
[<CompiledName("MyMethod")>]для контроля имени метода. - Помечайте модули как
[<RequireQualifiedAccess>], чтобы избежать конфликтов имён. - Для коллекций используйте
ResizeArray<T>(синонимList<T>из .NET), если нужна мутабельность.